Skip to content

Add feed hero section with carousel, highlights, and ad placement - #6515

Open
tsahimatsliah wants to merge 28 commits into
mainfrom
claude/hero-carousel-layout-e5e06u
Open

Add feed hero section with carousel, highlights, and ad placement#6515
tsahimatsliah wants to merge 28 commits into
mainfrom
claude/hero-carousel-layout-e5e06u

Conversation

@tsahimatsliah

@tsahimatsliah tsahimatsliah commented Aug 24, 2026

Copy link
Copy Markdown
Member

Changes

Introduces a new hero section component for the feed that displays:

  • A carousel of featured posts (top 4 headlines) with navigation controls
  • A "Happening Now" highlights list alongside the carousel
  • An ad placement in the rail (when available and user is not Plus)
  • Responsive layout: two-column on laptop+, stacked on mobile/tablet
  • Support for multiple post types (article, share, collection, freeform) with type-specific card renderers

New Components

  • FeedHero: Container component that fetches headlines and featured posts, manages ad queries and event logging
  • FeedHeroSection: Layout wrapper combining carousel, highlights, and ad placement
  • FeedHeroCarousel: Post carousel with indicator dots and prev/next navigation
  • FeedHeroAd: Ad card optimized for the rail placement
  • FeedSectionToolbar: Reusable toolbar for feed section headers (search, filters, bookmarks)
  • PostTypeToListCard: Mapping of post types to list card components (mirrors existing PostTypeToWideCard)

Integration

  • Wired into MainFeedLayout for My Feed and Popular pages when featureFeedHero flag is enabled
  • Feed component now accepts disableHighlightCards prop to prevent duplicate highlights when the hero section displays them
  • Comprehensive Storybook stories with mocks covering all component states and responsive breakpoints

Feature Flag

  • featureFeedHero: Controls hero section visibility on main feed pages (defaults to disabled)

Manual Testing

  • Carousel navigation (dots, prev/next buttons) wraps correctly
  • Responsive layout verified at mobile (390px), tablet (768px), laptop (1024px), desktop (1440px)
  • Ad placement renders with/without image, tags, advertiser info
  • Highlights list scrolls independently when content exceeds rail height
  • Post type variants (article, share, collection, freeform) render correctly
  • Feature flag integration prevents duplicate highlights in feed grid

https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa

Preview domain

https://claude-hero-carousel-layout-e5e0.preview.app.daily.dev

claude added 5 commits August 23, 2026 20:58
Adds a hero section for the top of the feed, plus a Storybook page to
iterate on it:

- FeedHeroCarousel: pages through featured posts with the existing
  featured-wide cards, dot indicators and prev/next chips labelled with
  the neighbouring headlines.
- FeedHeroAd: a compact native/direct ad placement for the rail, built
  from the existing ad primitives (link, image, attribution, pixel,
  viewability).
- FeedHeroSection: carousel on the left, rail on the right with the ad
  slot, the Happening Now highlights card and an Explore all CTA.
- FeedSectionToolbar: section title with search, filter and bookmark
  actions for the row above the tag chips.

Nothing renders this yet — the layout lives in Storybook under
Features/Feed/Hero, composed with the existing ExploreChipsBar and feed
cards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
The rail placement led with a call-to-action button that dominated a
20rem card, even though the whole card is already a click target. It now
carries the same elements as the feed ad card and nothing else:
advertiser logo and disclosure, the ad title, matching tags, and the
cover image in its own column.

Also adds the Storybook coverage needed to review it: responsive
breakpoints rendered in per-width iframes, hero section states (no ad,
single post, short highlights list), one carousel case per post type,
ad placement variants, and toolbar variants.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
…obile

Follow-up on the hero section review:

- Drops the "Explore all" button; the highlights card already ends in
  "Read all".
- Flattens the rail: the ad slot and the highlights card lose their
  background and border, keeping only a hover tint on the ad.
- Replaces the two headline-labelled paging buttons with a position
  counter and two arrow buttons, right-aligned. The headline moves to a
  tooltip and stays in the button's aria-label.
- Renders the featured post through the feed's own list card below
  laptop, so the top of the mobile feed matches the rest of it. The
  post-type-to-list-card map moves out of FeedItemComponent into
  cards/common/listCards.ts so both callers share one map.
- Renames the section heading to "Recommended for you" in the story.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
The dots on the left already show which post is showing, so the counter
next to the arrows was a second readout of the same thing. Shortens the
section heading to "Recommended" in the story too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
Puts the hero section on the real feed so it can be reviewed with live
data. Off by default behind the `feed_hero` flag.

- FeedHero fetches the six current `majorHeadlines` for the Happening Now
  list, then loads the top four of those headlines' posts in full through
  `feedByIds` for the carousel, keeping the headline order.
- The rail ad comes from the feed ad placement, skipped for Plus, and
  logs impression, viewable impression and click.
- Upvote, downvote, bookmark and copy link run the same mutations the
  feed cards use; post clicks log with a feed origin.
- MainFeedLayout mounts it in the feed's own topContent slot on My feed
  and Popular, and tells the feed to stop inserting its Happening Now
  card so the headlines are not listed twice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
@vercel

vercel Bot commented Aug 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
daily-webapp Ready Ready Preview Sep 3, 2026 10:55am UTC
1 Skipped Deployment
Project Deployment Actions Updated
storybook Ignored Ignored Sep 3, 2026 10:55am UTC

Request Review

A preview deploy is a production build, so GrowthBook devtools can't
force `feed_hero` there and the section stays invisible for review.
`?feed_hero=1` turns it on for that browser and remembers it,
`?feed_hero=0` turns it back off.

To be removed once the flag is configured in GrowthBook — a URL that
opts someone into an experiment arm would skew the allocation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
Review feedback on the live feed:

- Caps the section at 80rem and centres it; on a wide monitor it ran the
  full feed width.
- The featured card's cover now fills its column, inset with its own
  corners like the feed cards, instead of being letterboxed over a
  blurred backdrop. Gated on `coverImage` so the in-feed wide cards keep
  the treatment they ship with.
- Rebuilds the ad slot at the size of the two promo cards it replaces: a
  banner cover, then advertiser, title and tags.
- Adds a `compact` mode to the highlights content for a surface with no
  card chrome: smaller heading, tighter rows, and a fade at the bottom of
  the scroll area so the last row isn't sliced by the pinned footer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
- The ad slot now runs advertiser, title, tags, then the cover, matching
  the order of the feed cards.
- The featured card's summary was clamped at three lines, leaving a gap
  above the action row on a card that is much taller than a feed card. It
  runs to six in the hero, behind `expandDescription` so the in-feed wide
  cards keep the shorter clamp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
The carousel now rotates on its own. The active indicator fills over the
slide's duration and the slide advances on that animation's `animationend`,
so pausing the fill pauses the rotation: hovering or focusing anywhere in
the hero freezes it mid-count and leaving resumes from the same point
instead of restarting. Reduced motion drops the animation, which also stops
the rotation. Only a change the reader asked for is announced, so an
automatic rotation doesn't talk over a screen reader every few seconds.

The paging arrows drop to the tertiary variant so they sit behind the
content rather than beside it.

In the rail, the ad, the highlight rows and "Read all" now share one text
edge on both sides. The ad bleeds past it and is clipped by the column, so
its hover surface reads as a full-width band like the rows above it, and
the footer link goes flat instead of carrying its own inset pill. The ad's
tracking pixel moves out of flow — it was a zero-size flex item still
claiming a full gap — which, with a tighter column gap, closes most of the
dead space between the ad and Happening Now. The hero and the rail get a
little more room between them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
…ing Now

The slide being replaced now stays mounted on top of the new one until its
fade finishes, so the two cross over instead of the card popping. Measured
in a browser, the pair never drops below full opacity mid-transition, and
the outgoing slide unmounts on its own `animationend` — hence a near-zero
duration under reduced motion rather than no animation at all, which would
leave it mounted forever.

The rail becomes one list: "Happening Now" heads the column and the ad is
its first row, sharing the headlines' row treatment with the cover on the
right at the cards' ratio. Reclaiming the space the stacked ad took brings
the list from three visible headlines to five.

The hero's cover grows to two thirds of the card, which leaves the text a
third — narrow enough that the headline needed its own size, so `coverImage`
and `expandDescription` collapse into one `hero` flag carrying the cover
crop, the headline size and the summary clamp together. That split only
holds where the text column can still hold a headline: below 1360px the
hero falls back to an even split, which on a 1024px laptop is the
difference between a full headline and a truncated one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
…eadline

The cover was taking two thirds of the hero and squeezing the headline, so
the wide cards gain a five-column span: the only ratio here where the text
needs more than one column, hence a text-span map alongside the image one.
Below 1360px the even split still stands.

The ad drops into the Happening Now list as the second row rather than the
first, so the freshest headline keeps the lead — and as the first row only
when there are no headlines to sit under. It now carries a headline's shape:
the copy on top, and the disclosure under it in the colour the rows give
their timestamps, verified as the same computed value in both themes rather
than by eye. The advertiser mark shrinks to the meta line's scale, the cover
narrows, and the spacing around both opens up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
… card

A five-line headline overran the hero's fixed-height card by about 7px, and
`overflow: hidden` took the difference out of the action row, leaving a
sliced strip of buttons along the bottom edge. The summary now gives way
instead: it is the only shrinkable child, so the headline keeps every line
and the actions keep their place.

The opt-in is explicit because `base.css` resets everything to
`flex-shrink: 0`, which is what let the overflow reach the actions rather
than being absorbed higher up.

Shrinking alone cut the last line mid-glyph, so the text block reserves a
line of bottom padding under a matching fade: at full height the gradient
covers only the padding and the last line stays solid, and once the block is
squeezed the padding goes first and the cut line fades out. The padding sits
on the block rather than the summary because `overflow: hidden` clips at the
padding edge, which would leak a seventh line past the six-line clamp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
The rail gains a full-size direct placement below Happening Now, carrying
the feed ad card's own design with its background and border removed so it
sits on the page rather than in a box. Its inset goes with the chrome: the
creative lines up with the headline rows above it instead of standing 16px
inside them.

The markup comes out of `AdGrid` into `AdCardContent` so both placements
render one design rather than two copies. `AdGrid` keeps the card shell and
the auto-rotation, which stays behind: the hero renders inside the feed's
`ActiveFeedContext`, so rotating here would refetch against the feed's own
ad cache and overwrite a slot the feed still owns.

Each slot holds its own query key so the two ask the ad server separately
and can come back with different creatives, and each logs its own
impression, click and viewability under its own slot name.

The rail now runs past the carousel rather than the two columns sharing one
height, so the headline list keeps all five rows. The cost is dead space
under the carousel, which grows with the card.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
The hero drops its own width. The feed container already caps this box at
the card grid's width, which tracks the reader's column count, so the fixed
cap was both too wide at three columns and too narrow at five; without it
the section measures exactly the grid beneath it.

The rail's hover surfaces were bleeding past their column and being clipped
by it, which squared off every corner they had. They now sit inside the
column with the padding and the radius the feed cards use, so the shape
under the cursor is a rounded card rather than a full-bleed band. The ad
card takes the same treatment, and with the chrome back its content keeps
the feed card's own inset — which retires the flush option added for the
version that had none.

More padding costs a row: the headline list shows four now rather than five.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
The headline list goes back to being only headlines: the ad that sat in
it as a second row is gone, and the five rows it displaced return.

The remaining placement is rebuilt to the rail's own measurements rather
than the feed cell's, which is what made it read as a transplant: one
16px text edge shared with the headline rows beside it, their row radius
on the hover surface, and the disclosure under the copy in the colour
those rows give their timestamps. The cover comes down from 160px to
128px.

Three of the feed card's controls overran a 270px column by 50px, which
put "Remove" outside the card. "Advertise here" goes -- it is the one a
reader has least use for, and every ad card in the feed below still
carries it -- and the call to action can now shrink, so a long one
truncates instead of pushing its neighbour out.

The card also takes its own height instead of the column's. Ad copy is
short and the cover is deliberately modest, so stretching to the
section's height bought nothing but a 190px gap above the action row.

With one placement left, the two slots collapse to a single query that
the feed underneath can read as well, and the grid stands its own first
ad down while the hero is showing one -- shifted rather than skipped, so
the creative that slot would have shown moves to the next one instead of
being fetched and thrown away.

`AdCardContent` goes back into `AdGrid`. It was extracted so both
placements could render one design; they are now deliberately different,
and the indirection was buying nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa

Copy link
Copy Markdown
Member Author

test_shared is red on 837fc2c, and it is not this PR's.

The failure is src/hooks/usePlusSale.spec.tsx:

● usePlusSale › is active with a configured campaign and exposes its copy
  expect(result.current.isActive).toBe(true)
  Expected: true
  Received: false

The runningSale fixture hardcodes endDate: '2026-09-01T00:00:00.000Z'. That date passed on 1 September, so usePlusSale now correctly reports the campaign as inactive and the assertion fails. A time bomb that went off — nothing in the hook or the fixture changed, the calendar did. It fails the same way on main, so it is currently red on every open PR.

Running the full shared suite locally on this branch's exact tree: 2598 passed, 1 failed — that one. usePlusSale is not in this PR's diff.

Re-running the job would fail identically, since the failure is deterministic on the current date rather than flaky, so I have not spent a re-run on it.

The fix is one line, and I verified it locally (all 8 tests in the file pass, including the expiry case, which sets its own relative date and is unaffected):

-  endDate: '2026-09-01T00:00:00.000Z',
+  // Relative so the fixture can't expire on the calendar: a running campaign is
+  // one whose end date is still ahead. The expiry case below sets its own.
+  endDate: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),

I have deliberately not pushed it here — it is unrelated to this PR and belongs in its own change so it can land for everyone rather than waiting on this review. Happy to open that separately if useful.


Generated by Claude Code

…ail rules

The placement was built to a 300px column and then shown in one nearly
150px wider, where a 16px headline and a 128px cover read as a widget
parked next to two big columns. It now carries the featured card's scale
-- the same headline size, a full-size advertiser mark -- and runs the
section's height again, with the cover taking whatever the copy leaves
rather than sitting at a fixed size. That is what closes the gap the
fixed cover opened above the action row last time, and it is the reason
the card can be full height without one: 226px of cover at three columns,
286px when the copy is short.

The headline rows lose their `border-b`. A bottom border follows the row's
corner radius and curves up at both ends, drawing a half-open box around
every headline instead of a rule between them. The separator is now drawn
inset to the text edge, and the last row does without one.

"Read all" regains the bottom inset that lands it on the action row's
line, now that there is an action row at that height to meet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
…ard's order

The v2 grid sits inset inside the floating card and the hero is its sibling,
not its child, so the hero ran 24px wider on both sides than the cards under
it and sat flush to the top edge. It now carries the same inset, which moves
out of the grid's class list into a constant the two share. Its bottom margin
is what is left of the grid's 32px row gap once the grid's own top inset is
counted, so the hero stands off the first row the way the rows stand off each
other.

The ad follows the featured card beside it in order as well as scale: the
advertiser mark on top at the size that card gives its source, the headline,
its tags, then the disclosure where the card puts its date. The feed's two
controls close it. The creative's own call to action is left out -- a third
button wrapped the row onto two lines in a 270px column, and the whole card
is already the click target, so it was a second route to the same link.

Responsive pass over every breakpoint from 320 to 1976:

- The headline list's bottom fade only applies from `laptop` now. It exists so
  the pinned footer doesn't slice a row flat while the list scrolls, but below
  `laptop` the section stacks, every row fits, and it was dimming the last
  headline for nothing.
- The cover is fixed below `laptop`, where the column runs the full width and
  letting the creative keep its own aspect put a 600px image on the page.
- Whether the ad has a column is one rule in one place now, shared by the hook
  and the section, so a story can't lay out a fourth track the feed would
  never render.

Storybook gets the product's own breakpoints in the toolbar's device picker,
each boundary bracketed by the last width before it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
Picks up the fix for the expired `usePlusSale` fixture, which had been the
one red job on this branch.

Two conflicts in FeedItemComponent, both from main renaming or removing what
this branch had moved:

- main gave the inline list-card map a name, `PostTypeToTagList`. This branch
  had already lifted the same map into `cards/common/listCards.ts` so the hero
  and the feed share one, so main's copy goes and the shared one stays.
- Live rooms are gone from main -- the cards, the post type, the directory --
  and the shared list map still mapped `PostType.LiveRoom`, which no longer
  exists. Dropped there too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
It was carrying the headline rows' 12px radius, matched to the rail it sits
in. It is a card, though, and the cards it sits beside -- the feed's posts and
the highlights card, both on `Card` -- round at 16px. The cover keeps 12px,
which is what `CardImage` gives the feed card's own.

The rows keep theirs: a list row rounds tighter than a card here, the way the
highlights card's own rows do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
The carousel's dots and arrows ran 12px below "Read all" and the ad's
controls. The other two columns each hold a bottom inset that keeps their
footers off the section's edge — the rail on its "Read all" wrapper, the ad
through its card padding — and the carousel column had none, so its controls
went all the way down.

Measured after: the dots, both paging arrows, "Read all" with its arrow, and
"Advertise here"/"Remove" all centre on 484, and every control box is 32px.
The card gives up the 12px, which the summary already knows how to absorb —
the long-headline case still clears its action row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
The hero opens with a full-size featured card, and the grid beneath it could
open with a second one — two big cards stacked, which reads as a repeat.

The existing gate is `startIndex`, which counts items, so at four or five
columns the first item is still in the opening row and no value keeps a wide
card out of it without also suppressing the ones further down. The placement
builder already tracks the row it is on, so the floor is now stated in rows:
`minWideCardRow`, defaulting to 0, and set to 1 while the hero is on. Both
builders in `useFeed` take it, or the ad cadence and the rendered grid would
disagree about the same card.

Wide cards resume in the second row and below, with the density cap and the
fit-to-row clamp unchanged — a card widening at column 1 of four still takes
the three columns left to it, not four.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants